#!/usr/bin/env python3

# Copyright (c) 2020-2021 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import re
import abc
import stat
import pathlib
from ._util import Util, ActionRunner
from ._errors import WorkDirError
from ._device import DeviceManager


class WorkDir:
    """
    This class manipulates lineage-stage4's working directory.
    """

    def __init__(self, path, reset=False):
        assert path is not None

        self._MODE = 0o40700

        self._path = path
        self._deviceFile = os.path.join(self._path, "device.save")

        self._persistentStorage = WorkDirPersisentStorage(self)

        if not os.path.exists(self._path):
            os.mkdir(self._path, mode=self._MODE)
        else:
            # work directory can be a directory or directory symlink
            # so here we use os.stat() instead of os.lstat()
            s = os.stat(self._path)
            if not stat.S_ISDIR(s.st_mode):
                raise WorkDirError("\"%s\" is not a directory" % (self._path))
            if s.st_mode != self._MODE:
                raise WorkDirError("invalid mode for \"%s\"" % (self._path))
            if s.st_uid != os.getuid():
                raise WorkDirError("invalid uid for \"%s\"" % (self._path))
            if s.st_gid != os.getgid():
                raise WorkDirError("invalid gid for \"%s\"" % (self._path))

            # clear directory content if needed
            if reset:
                Util.removeDirContentExclude(self._path, [])

        self._addon = None
        if os.path.exists(self._deviceFile):
            osVersion, deviceId = self._parseDeviceFileContent(self._deviceFile)
            self._addon = DeviceManager.getModule(deviceId, "_build").WorkDirAddon(self, osVersion)
            assert isinstance(self._addon, WorkDirAddonBase)

    @property
    def path(self):
        return self._path

    def has_error(self):
        assert not self._persistentStorage.isInUse()
        _, err = self._persistentStorage.initGetCurrentActionInfo()
        return err is not None

    def get_error_message(self):
        assert not self._persistentStorage.isInUse()
        actionName, err = self._persistentStorage.initGetCurrentActionInfo()
        return "action %s failed (%s) for \"%s\"" % (actionName, err, self._path)

    def get_latest_action_dirpath(self):
        # note: may return None
        return self._persistentStorage.getLastActionDirIndexName()[0]

    def is_build_finished(self):
        return self._persistentStorage.isFinished()

    def save_device_and_attach_addon(self, osVersion, deviceId):
        if self._addon is not None:
            assert (osVersion, deviceId) == self._parseDeviceFileContent(self._deviceFile)
        else:
            self._addon = DeviceManager.getModule(deviceId, "_build").WorkDirAddon(self, osVersion)
            assert isinstance(self._addon, WorkDirAddonBase)
            with open(self._deviceFile, "w") as f:
                f.write(self._generateDeviceFileContent(osVersion, deviceId))

    @staticmethod
    def _parseDeviceFileContent(path):
        m = re.fullmatch("(.*)\n(.*)\n", pathlib.Path(path).read_text())
        return (m.group(1), m.group(2))                                     # (osVersion, deviceId)

    @staticmethod
    def _generateDeviceFileContent(osVersion, deviceId):
        buf = ""
        buf += osVersion + "\n"
        buf += deviceId + "\n"
        return buf

    def __getattr__(self, attr):
        if self._addon is not None:
            return getattr(self._addon, attr)
        raise AttributeError(f"'{self.__class__.name}' object has no attribute '{attr}'")       # ugly, from PEP-562, a string which is same as system error message


class WorkDirAddonBase(abc.ABC):

    def __init__(self):
        self._path = None


class WorkDirPersisentStorage(ActionRunner.PersistStorage):

    def __init__(self, parent):
        self._parent = parent
        self._errFile = os.path.join(self._parent.path, "error.save")
        self._finishFile = os.path.join(self._parent.path, "finished.flag")
        self._inUse = False

    def initGetCurrentActionInfo(self):
        _, _, actionName = self.getLastActionDirIndexName()

        error = None
        try:
            error = pathlib.Path(self._errFile).read_text().rstrip("\n")
        except FileNotFoundError:
            pass

        if actionName is not None:
            if error == "":
                error = "crashed"

        return (actionName, error)

    def getLastActionDirIndexName(self):
        fnList = os.listdir(self._parent.path).sort()
        if len(fnList) == 0:
            return (None, None, None)
        else:
            m = re.fullmatch("([0-9]+)-(.*)", fnList[-1])
            return (m.group(0), int(m.group(1)), m.group(2))

    def getHistoryActionNames(self):
        ret = []
        for fn in os.listdir(self._parent.path).sort():
            m = re.fullmatch("[0-9]+-(.*)", fn)
            ret.append(m.group(1))
        return ret

    def isFinished(self):
        return os.path.exists(self._finishFile)

    def isInUse(self):
        return self._inUse

    def use(self):
        assert not self._inUse
        self._inUse = True

    def saveActionStart(self, actionName):
        assert self._inUse
        assert not os.path.exists(self._errFile)

        oldActionDir, oldActionIndex, _ = self.getLastActionDirIndexName()
        if oldActionDir is None:
            os.mkdir("00-" + actionName)
        else:
            os.rename(oldActionDir, "%d-%s" % (oldActionIndex + 1, actionName))
            os.mkdir(oldActionDir)

        with open(self._errFile, "w") as f:
            f.write("")

    def saveActionEnd(self, error=None):
        assert self._inUse
        assert os.path.exists(self._errFile)

        if error is None:
            os.unlink(self._errFile)
        else:
            with open(self._errFile, "w") as f:
                f.write(error + "\n")

    def saveFinished(self):
        assert self._inUse
        assert not os.path.exists(self._finishFile)
        with open(self._finishFile, "w") as f:
            f.write("")

    def unUse(self):
        assert self._inUse
        self._inUse = False
